/*
Copyright (C) 1996-1997 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 3
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// common.c -- misc functions used in client and server

#include "quakedef.h"

cvar_t *registered;
cvar_t *cmdline;

BOOL com_modified; // set TRUE if using non-id files

int static_registered = 1; // only for startup check, then set

BOOL msg_suppress_1 = 0;

// if a packfile directory differs from this, it is assumed to be hacked
#define PAK0_COUNT		339
#define PAK0_CRC		32981

BOOL standard_quake = TRUE, rogue, hipnotic, quoth;

/*

All of Quake's data access is through a hierarchical file system, but the contents of the file system can be transparently merged from several
sources.

The "base directory" is the path to the directory holding the quake.exe and all game directories.  The sys_* files pass this to host_init in
quakeparms_t->basedir.  This can be overridden with the "-basedir" command line parm to allow code debugging in a different directory.  The
base directory is only used during filesystem initialization.

The "game directory" is the first tree on the search path and directory that all generated files (savegames, screenshots, demos, config files)
will be saved to.  This can be overridden with the "-game" command line parameter.  The game directory can never be changed while quake is
executing.  This is a precacution against having a malicious server instruct clients to write files over areas they shouldn't.

The "cache directory" is only used during development to save network bandwidth, especially over ISDN / T1 lines.  If there is a cache directory
specified, when a file is found by the normal search path, it will be mirrored into the cache directory, then opened there.


FIXME:
The file "parms.txt" will be read out of the game directory and appended to the current command line arguments to allow different games to
initialize startup parms differently.  This could be used to add a "-sspeed 22050" for the high quality sound edition.  Because they are added
at the end, they will not override an explicit setting on the original command line.

*/


//============================================================================

searchpath_t *com_searchpaths;

BOOL FS_CreateDirectory (char *path)
{
	char dirname[MAX_PATH];

	// compose a full path
	sprintf (dirname, "%s/%s", com_gamedir, path);

	// convert '/' to '\\'
	for (int i = 0; ; i++)
	{
		if (!dirname[i]) break;
		if (dirname[i] == '/') dirname[i] = '\\';
	}

	// see is it a file
	for (int i = strlen (dirname); i; i--)
	{
		if (dirname[i] == '.')
		{
			// remove the last element
			for (; i; i--)
			{
				if (dirname[i] == '\\')
				{
					dirname[i] = 0;
					break;
				}
			}

			break;
		}
	}

	// com_gamedir alredy exists so don't bother creating up to this point
	for (int i = strlen (com_gamedir) + 1; ; i++)
	{
		if (!dirname[i]) break;

		if (dirname[i] == '/' || dirname[i] == '\\')
		{
			// create up to this point
			char c = dirname[i];

			// null-terminate it at this point
			dirname[i] = 0;

			// and attempt to create it
			if (!CreateDirectory (dirname, NULL))
				if (GetLastError () != ERROR_ALREADY_EXISTS)
					return FALSE;

			// restore the old separator
			dirname[i] = c;
		}
	}

	// one more create for the last element
	if (!CreateDirectory (dirname, NULL))
		if (GetLastError () != ERROR_ALREADY_EXISTS)
			return FALSE;

	// success
	return TRUE;
}


/*
============
FS_SkipPath
============
*/
char *FS_SkipPath (char *pathname)
{
	char *last = pathname;

	while (*pathname)
	{
		if (*pathname == '/')
			last = pathname + 1;

		pathname++;
	}

	return last;
}

/*
============
FS_StripExtension
============
*/
void FS_StripExtension (char *in, char *out)
{
	while (*in && *in != '.')
		*out++ = *in++;

	*out = 0;
}

/*
============
FS_FileExtension
============
*/
char *FS_FileExtension (char *in)
{
	static char exten[8];
	int i;

	while (*in && *in != '.')
		in++;

	if (!*in)
		return "";

	in++;

	for (i = 0; i < 7 && *in; i++, in++)
		exten[i] = *in;

	exten[i] = 0;
	return exten;
}

/*
============
FS_FileBase
============
*/
void FS_FileBase (char *in, char *out)
{
	char *s, *s2;

	s = in + strlen (in) - 1;

	while (s != in && *s != '.')
		s--;

	for (s2 = s; *s2 && *s2 != '/'; s2--);

	if (s - s2 < 2)
		strcpy (out, "?model?");
	else
	{
		s--;
		strncpy (out, s2 + 1, s - s2);
		out[s - s2] = 0;
	}
}


/*
==================
FS_DefaultExtension
==================
*/
void FS_DefaultExtension (char *path, char *extension)
{
	char *src;

	// if path doesn't have a .EXT, append extension
	// (extension should include the .)
	src = path + strlen (path) - 1;

	while (*src != '/' && src != path)
	{
		if (*src == '.')
			return;                 // it has an extension

		src--;
	}

	strcat (path, extension);
}


/*
=============================================================================

QUAKE FILESYSTEM

=============================================================================
*/

int com_filesize;

#define MAX_FILES_IN_PACK 2048

char com_gamedir[MAX_OSPATH];

dpackfile_t *FS_FindFileInPAK (dpackfile_t *files, int numfiles, char *filename)
{
	// to do - we could potentially short-circuit some of this by defining a range to search in based on the first char of the filename
	int imin = 0;
	int imax = numfiles - 1;
	int imid, comp;

	for (;;)
	{
		if (imax < imin) break;

		// both names were force-lower-cased so that we don't need to do a case-insensitive match here
		imid = (imax + imin) >> 1;
		comp = strcmp (files[imid].name, filename);

		if (comp < 0)
			imin = imid + 1;
		else if (comp > 0)
			imax = imid - 1;
		else return &files[imid];
	}

	// not found
	return NULL;
}


/*
============
FS_Path_f

============
*/
void FS_Path_f (void)
{
	searchpath_t    *s;

	Con_Printf (PRINT_DEFAULT, "Current search path:\n");

	for (s = com_searchpaths; s; s = s->next)
	{
		if (s->pack)
			Con_Printf (PRINT_DEFAULT, "%s (%i files)\n", s->pack->filename, s->pack->numfiles);
		else Con_Printf (PRINT_DEFAULT, "%s\n", s->filename);
	}
}


/*
===========
FS_FindFile

Finds the file in the search path.
Sets com_filesize and one of handle or file
===========
*/
int FS_FindFile (char *filename, int *handle, FILE **file)
{
	if (file && handle)
	{
		Sys_Error ("FS_FindFile: both handle and file set");
		return -1;
	}

	if (!file && !handle)
	{
		Sys_Error ("FS_FindFile: neither handle or file set");
		return -1;
	}

	// make two passes through the search; the first pass looks in the filesystem; the second in PAK files
	for (searchpath_t *search = com_searchpaths; search; search = search->next)
	{
		if (search->pack) continue;

		// check a file in the directory tree
		if (!static_registered)
		{
			// if not a registered version, don't ever go beyond base
			if (strchr (filename, '/') || strchr (filename, '\\'))
				continue;
		}

		char netpath[MAX_OSPATH];

		sprintf (netpath, "%s/%s", search->filename, filename);

		int i;
		int findtime = Sys_FileTime (netpath);

		if (findtime == -1)
			continue;

		com_filesize = Sys_FileOpenRead (netpath, &i);

		if (handle)
			*handle = i;
		else if (file)
		{
			Sys_FileClose (i);
			*file = fopen (netpath, "rb");
		}

		return com_filesize;
	}

	// now search in PAKs
	for (searchpath_t *search = com_searchpaths; search; search = search->next)
	{
		// is the element a pak file?
		if (!search->pack) continue;

		// instead of a linear walk this does a binary search on the pak file
		// (which must have it's contents pre-sorted) for faster loading
		pack_t *pak = search->pack;
		dpackfile_t *pf = FS_FindFileInPAK (pak->files, pak->numfiles, filename);

		if (pf)
		{
			if (handle)
			{
				*handle = pak->handle;
				Sys_FileSeek (pak->handle, pf->filepos);
			}
			else
			{
				// open a new file on the pakfile
				*file = fopen (pak->filename, "rb");

				if (*file)
					fseek (*file, pf->filepos, SEEK_SET);
			}

			com_filesize = pf->filelen;
			return com_filesize;
		}
	}

	// didn't find it
	if (handle)
		*handle = -1;
	else *file = NULL;

	com_filesize = -1;
	return -1;
}


/*
===========
FS_OpenFile

filename never has a leading slash, but may contain directory walks
returns a handle and a length
it may actually be inside a pak file
===========
*/
int FS_OpenFile (char *filename, int *handle)
{
	return FS_FindFile (filename, handle, NULL);
}

/*
===========
FS_FOpenFile

If the requested file is inside a packfile, a new FILE * will be opened
into the file.
===========
*/
int FS_FOpenFile (char *filename, FILE **file)
{
	return FS_FindFile (filename, NULL, file);
}

/*
============
FS_CloseFile

If it is a pak file handle, don't really close it
============
*/
void FS_CloseFile (int h)
{
	searchpath_t    *s;

	for (s = com_searchpaths; s; s = s->next)
		if (s->pack && s->pack->handle == h)
			return;

	Sys_FileClose (h);
}


/*
============
FS_LoadFile

Loads a file into an explicit memory pool
Filename are reletive to the quake directory.
Allways appends a 0 byte.
============
*/
byte *FS_LoadFileCommon (char *path, int h, byte *buf, int len)
{
	char base[MAX_OSPATH];

	if (!buf)
	{
		Sys_Error ("FS_LoadFile : not enough space for %s", path);
		return NULL;
	}

	// extract the filename base name for hunk tag
	FS_FileBase (path, base);

	((byte *) buf)[len] = 0;

	Draw_BeginDisc ();
	Sys_FileRead (h, buf, len);
	FS_CloseFile (h);
	Draw_EndDisc ();

	return buf;
}


byte *FS_LoadFile (char *path, QALLOCATORBASE *allocator)
{
	// look for it in the filesystem or pack files
	int h = 0;
	int len = FS_OpenFile (path, &h);

	if (h == -1)
		return NULL;
	else return FS_LoadFileCommon (path, h, (byte *) allocator->FastAlloc (len + 1), len);
}


/*
=================
FS_LoadPackFile

Takes an explicit (not game tree related) path to a pak file.

Loads the header and directory, adding the files at the beginning
of the list so they override previous pack files.
=================
*/
int FS_SortPackFile (dpackfile_t *p1, dpackfile_t *p2)
{
	return strcmp (p1->name, p2->name);
}


pack_t *FS_LoadPackFile (char *packfile)
{
	dpackheader_t header;
	int i;
	dpackfile_t *newfiles;
	int numpackfiles;
	pack_t *pack;
	int packhandle;
	unsigned short crc;

	if (Sys_FileOpenRead (packfile, &packhandle) == -1)
		return NULL;

	Sys_FileRead (packhandle, (void *) &header, sizeof (header));

	if (header.id[0] != 'P' || header.id[1] != 'A' || header.id[2] != 'C' || header.id[3] != 'K')
		Sys_Error ("%s is not a packfile", packfile);

	header.dirofs = LittleLong (header.dirofs);
	header.dirlen = LittleLong (header.dirlen);

	numpackfiles = header.dirlen / sizeof (dpackfile_t);

	if (numpackfiles > MAX_FILES_IN_PACK)
		Sys_Error ("%s has %i files", packfile, numpackfiles);

	if (numpackfiles != PAK0_COUNT)
		com_modified = TRUE;    // not the original file

	newfiles = (dpackfile_t *) gamePool->Alloc (numpackfiles * sizeof (dpackfile_t));

	Sys_FileSeek (packhandle, header.dirofs);
	Sys_FileRead (packhandle, (void *) newfiles, header.dirlen);

	// crc the directory to check for modifications
	CRC_Init (&crc);

	for (i = 0; i < header.dirlen; i++)
		CRC_ProcessByte (&crc, ((byte *) newfiles)[i]);

	if (crc != PAK0_CRC)
		com_modified = TRUE;

	// swap the directory
	for (i = 0; i < numpackfiles; i++)
	{
		newfiles[i].filepos = LittleLong (newfiles[i].filepos);
		newfiles[i].filelen = LittleLong (newfiles[i].filelen);
	}

	// sort the pack by name for faster searching
	qsort (newfiles, numpackfiles, sizeof (dpackfile_t), (sortfunc_t) FS_SortPackFile);

	pack = (pack_t *) gamePool->Alloc (sizeof (pack_t));
	strcpy (pack->filename, packfile);
	pack->handle = packhandle;
	pack->numfiles = numpackfiles;
	pack->files = newfiles;

	Con_Printf (PRINT_DEFAULT, "Added packfile %s (%i files)\n", packfile, numpackfiles);
	return pack;
}


/*
================
FS_AddGameDirectory

Sets com_gamedir, adds the directory to the head of the path,
then loads and adds pak1.pak pak2.pak ...
================
*/
void FS_AddGameDirectory (char *dir)
{
	int i;
	searchpath_t *search;
	pack_t *pak;

	// detect a mission pack
	if (!strcmp (dir, "rogue")) rogue = TRUE;
	if (!strcmp (dir, "hipnotic")) hipnotic = TRUE;
	if (!strcmp (dir, "quoth")) quoth = TRUE;

	// either mission pack will disable the standard game
	if (rogue || hipnotic || quoth) standard_quake = FALSE;

	// quoth must also enable hipnotic for the sbar layout
	if (quoth) hipnotic = TRUE;

	sprintf (com_gamedir, "%s/%s", host_parms.basedir, dir);

	// add the directory to the search path
	search = (searchpath_t *) gamePool->Alloc (sizeof (searchpath_t));
	sprintf (search->filename, "%s/%s", host_parms.basedir, dir);
	search->next = com_searchpaths;
	com_searchpaths = search;

	// add any pak files in the format pak0.pak pak1.pak, ...
	for (i = 0; ; i++)
	{
		if ((pak = FS_LoadPackFile (va ("%s/%s/pak%i.pak", host_parms.basedir, dir, i))) == NULL) break;

		search = (searchpath_t *) gamePool->Alloc (sizeof (searchpath_t));
		search->pack = pak;
		search->next = com_searchpaths;
		com_searchpaths = search;
	}

	// add the contents of the parms.txt file to the end of the command line
}


void FS_CompleteLoadingGame (void)
{
	// create our default content folders
	FS_CreateDirectory ("save");
	FS_CreateDirectory ("screenshot");

	// enumerate content for this game
	FS_EnumerateContent ();
}


/*
================
FS_InitFilesystem
================
*/
void FS_InitFilesystem (void)
{
	// reset the gametype flags
	rogue = hipnotic = quoth = FALSE;
	standard_quake = TRUE;

	// start up with GAMENAME by default (id1)
	FS_AddGameDirectory (GAMENAME);

	if (COM_CheckParm ("-rogue")) FS_AddGameDirectory ("rogue");
	if (COM_CheckParm ("-hipnotic")) FS_AddGameDirectory ("hipnotic");
	if (COM_CheckParm ("-quoth")) FS_AddGameDirectory ("quoth");

	// -game <gamedir>
	// Adds basedir/gamedir as an override game
	int i = COM_CheckParm ("-game");

	if (i && i < com_argc - 1)
	{
		com_modified = TRUE;
		FS_AddGameDirectory (com_argv[i + 1]);
	}

	FS_CompleteLoadingGame ();
}


/*
================
COM_LoadGamePics

load all the pics specific to the current game; D3D and the filesystem must be already initialized
================
*/
void FS_LoadGame (void)
{
	// reload the palette
	if (!(vid.palette = (byte *) FS_LoadFile ("gfx/palette.lmp", gamePool))) Sys_Error ("Couldn't load gfx/palette.lmp");
	if (!(vid.colormap = (byte *) FS_LoadFile ("gfx/colormap.lmp", gamePool))) Sys_Error ("Couldn't load gfx/colormap.lmp");

	// rebuild the palettes for D3D
	VID_SetPalette (vid.palette);

	// and the gfx file
	W_LoadWadFile ("gfx.wad");

	// and now reload all images/etc
	Draw_InitPics ();
	SCR_InitPics ();
	Sbar_InitPics ();

	// and sounds
	Snd_InitSounds ();

	FS_CompleteLoadingGame ();

	// rearm the timers becuase this may take some time
	Host_RearmTimers ();

	// it's OK to do screen updates now
	scr.Initialized = TRUE;
}


void FS_UnloadGame (void)
{
	// disable all screen updates during the unload and reload phases
	scr.Initialized = FALSE;

	// disconnect from any currently running map
	CL_Disconnect_f ();

	// save out the current configs before we go down
	Host_WriteConfiguration ();

	// close all open file handles
	Sys_CloseAllHandles ();

	// free all allocated memory and trash the file system
	com_searchpaths = NULL;

	// trash memory allocations and wipe the cl and sv structs
	Host_ClearMemory ();

	// bump the registration sequence so that everything will expire
	// (this doesn't really matter as we're going to Release it all anyway...)
	r_RegistrationSequence += 10;

	// unbind all state
	// D3D10 FIXME
	//d3d_State->UnbindState ();

	// unload all game-specific D3D objects (including cached/persistent objects)
	Draw_ShutdownPics ();
	Tex_Shutdown ();
	Mesh_ShutdownBuffers ();

	// trash the cache heap and the model and sound caches
	Mod_TrashAll ();
	Snd_TrashAll ();

	// trashing the game pool is deferred to here because sounds and mdls now live in it
	gamePool->FreeToLowMark (0);
}


void FS_Game_f (void)
{
	if (Cmd_Argc () < 2)
	{
		Con_Printf (PRINT_DEFAULT, "%s : change game directory\n", Cmd_Argv (0));
		return;
	}

	// unload the game
	FS_UnloadGame ();

	// reset the gametype flags
	rogue = hipnotic = quoth = FALSE;
	standard_quake = TRUE;
	com_modified = FALSE;

	// build a new filesystem
	FS_AddGameDirectory (GAMENAME);

	// load the games in order
	for (int i = 1; i < Cmd_Argc (); i++)
	{
		// don't load id1 as a game if it's the first gamedir (it's always loaded above)
		// it can be specified subsequently to override gamedir content (e.g a broken gfx.wad but you want the maps)
		if (i == 1 && !stricmp (Cmd_Argv (i), "id1")) continue;

		// only reset com_modified here if a game is loaded
		com_modified = TRUE;
		FS_AddGameDirectory (Cmd_Argv (i));
	}

	// load the new game-specific data
	FS_LoadGame ();

	// re-exec the configs for the new game
	Cbuf_InsertText ("exec quake.rc\n");
}


void FS_Unpack_f (void)
{
	if (Cmd_Argc () != 2)
	{
		Con_Printf (PRINT_SAFE, "unpack <argstring> : unpacks all files beginning with the argstring\n");
		return;
	}

	int numunpack = 0;

	for (searchpath_t *search = com_searchpaths; search; search = search->next)
	{
		// is the element a pak file?
		if (search->pack)
		{
			// instead of a linear walk this does a binary search on the pak file
			// (which must have it's contents pre-sorted) for faster loading
			pack_t *pak = search->pack;
			char *mask = Cmd_Argv (1);

			for (int i = 0; i < pak->numfiles; i++)
			{
				// only if it matches
				if (strncmp (pak->files[i].name, mask, strlen (mask))) continue;

				// create a directory from the file name
				if (!FS_CreateDirectory (pak->files[i].name)) continue;

				// do a normal Con_Printf to update the screen (which will show progress as it happens)
				Con_Printf (PRINT_SAFE, "unpacking %s... ", pak->files[i].name);
				SCR_UpdateScreen (0);

				// tempalloc a buffer for reading into (done after the Con_Printf which triggers a screen update which may use qScratch)
				void *buf = (void *) qScratch->Lock (__FUNCTION__);
				FILE *f = NULL;
				char filename[MAX_PATH];

				// seek to the file and read it in
				Sys_FileSeek (pak->handle, pak->files[i].filepos);
				Sys_FileRead (pak->handle, buf, pak->files[i].filelen);

				// write it out
				sprintf (filename, "%s/%s", com_gamedir, pak->files[i].name);

				if ((f = fopen (filename, "wb")) != NULL)
				{
					fwrite (buf, pak->files[i].filelen, 1, f);
					fflush (f);
					fclose (f);

					// must unlock before the Con_Printf as that will try to use scratch
					qScratch->Unlock ();
					Con_Printf (PRINT_SAFE, "done\n");
					numunpack++;
				}
				else
				{
					// must unlock before the Con_Printf as that will try to use scratch
					qScratch->Unlock ();
					Con_Printf (PRINT_SAFE, "failed\n");
				}

				// run another screen update after the unpack to show the result message
				SCR_UpdateScreen (0);
			}
		}
	}

	// tell the user it's done as unpacking may take some time and they may think it's stalled
	Con_Printf (PRINT_SAFE, "Done - unpacked %i files\n", numunpack);
}


/*
================
FS_Init
================
*/
void FS_Init (char *basedir)
{
	Swap_Init ();

	registered = Cvar_Get ("registered", "0");
	cmdline = Cvar_Get ("cmdline", "0", CVAR_SERVER);

	Cmd_AddCommand ("path", FS_Path_f);
	Cmd_AddCommand ("game", FS_Game_f);
	Cmd_AddCommand ("gamedir", FS_Game_f);
	Cmd_AddCommand ("unpack", FS_Unpack_f);

	FS_InitFilesystem ();
	COM_CheckRegistered ();
}

